本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
檔案上傳(File Upload) 是一項很基本的功能,到處都可以看見它的蹤影,如:某某社群網站的上傳大頭貼、某某影音網站上傳影片等。
Nest 針對檔案上傳功能封裝了一套名為 multer 的套件,它會處理格式為 multipart/form-data
的資料,在 Express 的應用程式上經常可以看到它的身影,是非常知名的套件。
雖然 Nest 將其包裝成內建模組,但還是建議各位安裝 multer 的型別定義檔,透過 npm 來進行安裝:
$ npm install @types/multer -D
接收單一檔案的方式很簡單,只要在特定路由下使用 FileInterceptor
並透過參數裝飾器 @UploadedFile
來取得檔案。其中,FileInterceptor
有兩個參數可以帶入,分別是:
fieldName
:檔案在表單上對應的名稱。options
:對應到 MulterOption
,詳細內容可以參考 multer 官方文檔。這邊以 app.controller.ts
為例來實作單一檔案上傳:
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
@Post('/single')
@UseInterceptors(FileInterceptor('file'))
uploadSingleFile(@UploadedFile() file: Express.Multer.File) {
return file;
}
}
透過 Postman 進行測試,將一個檔案名稱為 nestjs_logo.svg
的圖片上傳,會收到該圖片的相關訊息:
如果同一個欄位名稱有一個以上的檔案,要使用 FilesInterceptor
並透過參數裝飾器 @UploadedFiles
來取得一個包含多個 Express.Multer.File
型別的陣列。
注意:這裡是使用複數 Files 而不是單一檔案上傳所使用的
FileInterceptor
與@UploadedFile
。
FilesInterceptor
有三個參數可以帶入,分別是:
fieldName
:檔案在表單上對應的名稱。maxCount
:配置可接受檔案數量的上限,可以選擇性填入。options
:對應到 MulterOption
。同樣以 app.controller.ts
為例來實作單一欄位多檔上傳:
import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
@Post('/multiple')
@UseInterceptors(FilesInterceptor('files'))
uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
return files.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
}
}
透過 Postman 進行測試,將檔案名稱為 nestjs_logo.svg
與 nodejs_logo.png
的圖片上傳,會收到它們的欄位名稱與檔案名稱:
假如表單有多個欄位並且有一個以上的欄位包含檔案,要使用 FileFieldsInterceptor
並透過 @UploadedFiles
裝飾器來取得一個以欄位名稱作為 key
的物件,其值為 Express.Multer.File
型別的陣列。其中,FileFieldsInterceptor
有兩個參數可以帶入:
uploadedFields
:一個包含多個物件的陣列,物件需要擁有 name
屬性來指定欄位的名稱,亦可以給定 maxCount
來指定該欄位可接受的檔案數量上限。options
:對應到 MulterOption
。同樣以 app.controller.ts
為例來實作多欄位多檔案上傳:
import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { FileFieldsInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
@Post('/multiple')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'first' },
{ name: 'second' }
]))
uploadMultipleFiles(@UploadedFiles() files: { [x: string]: Express.Multer.File[] }) {
const { first, second } = files;
const list = [...first, ...second];
return list.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
}
}
透過 Postman 進行測試,將檔案名稱為 nestjs_logo.svg
與 nodejs_logo.png
的圖片上傳,會收到它們的欄位名稱與檔案名稱:
假如表單有多個欄位並且有一個以上的欄位包含檔案,但不需要依照欄位名稱做分類的話,可以直接使用 AnyFilesInterceptor
並透過 @UploadedFiles
裝飾器來取得一個包含多個 Express.Multer.File
型別的陣列。其中,AnyFilesInterceptor
可以帶入一個參數,即 options
。
同樣以 app.controller.ts
為例來實作不分欄位多檔上傳:
import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AnyFilesInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
@Post('/multiple')
@UseInterceptors(AnyFilesInterceptor())
uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
return files.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
}
}
透過 Postman 進行測試,將檔案名稱為 nestjs_logo.svg
與 nodejs_logo.png
的圖片上傳,會收到它們的欄位名稱與檔案名稱:
上面每個功能都可以指定 MulterOption
的配置,假如有個配置是多數上傳檔案都會用到的,那每次都要個別配置實在太麻煩了,所以 Nest 有提供一個預設值的方法,大幅減少這種重複的操作,那該如何使用呢?只要導入 MulterModule
並調用 register
方法即可,該方法可接受之參數正是 MulterOption
。
這裡以 app.module.ts
為例,假如我們希望把上傳的檔案存到名為 upload
的資料夾裡,那就在 register
裡面給定 dest
屬性,並指定其值為 ./upload
:
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
MulterModule.register({
dest: './upload'
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
我們沿用「預設 multer 設置」與「不分欄位之多個檔案上傳」的範例進行測試,透過 Postman 上傳 nestjs_logo.svg
與 nodejs_logo.png
,會在專案目錄下看到 upload
資料夾,裡面含有以下內容:
奇怪,怎麼跟預期的不一樣?是哪裡出錯了嗎?其實這是因為 multer 預設情況下會隨機命名避免檔名衝突,這裡可以做個小實驗,將這兩個檔案的檔案名稱與副檔名改成原始的樣子,就可以看到他們的原貌了:
但這並不是解決問題的好方法,我們會希望能夠自動化去處理這件事情,那該怎麼做呢?這時候可以用 multer 提供的 diskStorage
來輔助我們去處理檔案名稱的問題。
diskStorage
是一個函式,我們可以透過指定 destination
來配置檔案的存放位置、指定 filename
去處理檔案名稱,這兩個屬性的值皆為 函式,透過函式去處理的彈性比較大,畢竟給特定值並不適用在每個場景。
我們透過撰寫一個 Helper Class 來實作這兩個函式,在 src
資料夾下新增 core/helpers
資料夾,並添加 multer.helper.ts
,由於這兩個函式有特定的參數,故我們的方法也需要遵循這些參數來設計,其包含了 Request
、Express/Multer.File
以及 (error: Error | null, destination: string) => void
的 Callback 函式,透過該 Callback 將處理好的結果返回給 multer:
import { Request } from 'express';
import { join } from 'path';
export class MulterHelper {
public static destination(
request: Request,
file: Express.Multer.File,
callback: (error: Error | null, destination: string) => void
): void {
callback(null, join(__dirname, '../../../upload/'));
}
public static filenameHandler(
request: Request,
file: Express.Multer.File,
callback: (error: Error | null, destination: string) => void
): void {
const { originalname } = file;
const timestamp = new Date().toISOString();
callback(null, `${timestamp}-${originalname}`);
}
}
接著,我們就來將這兩個函式實裝上去,修改 app.module.ts
的內容,將 register
物件參數中的 dest
換成 storage
,並配置 destination
與 filename
:
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { MulterHelper } from './core/helpers/multer.helper';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
MulterModule.register({
storage: diskStorage({
destination: MulterHelper.destination,
filename: MulterHelper.filenameHandler
})
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
最後,透過 Postman 進行測試,將 nestjs_logo.svg
與 nodejs_logo.png
上傳,會在專案目錄下的 upload
資料夾看到這兩個檔案:
multer 將檔案上傳功能簡化成套用 Middleware 即可使用,Nest 更進一步進行包裝,使其可以很輕易地在 Nest 中使用,讓它的使用方式更符合 Nest 的設計原則,是非常好用且強大的套件。這裡附上今天的懶人包:
multipart/form-data
的資料。FileInterceptor
並透過 @UploadedFile
來取得檔案資料。FilesInterceptor
並透過 @UploadedFiles
來取得檔案資料。FileFieldsInterceptor
並透過 @UploadedFiles
來取得檔案資料。AnyFilesInterceptor
並透過 @UploadedFiles
來取得檔案資料。MulterModule.register()
來配置 multer 預設值。storage
屬性與 diskStorage
來實作檔案儲存。